#include "text_layout.h"
#include "unicode.h"

void TextLayout::CheckIntegrity()
{
    wxASSERT(text_len>0 && regions.size()>0);

#ifndef NDEBUG
    for (unsigned x=0; x<regions.size(); x++)
    {
        wxASSERT(regions[x].start >= 0);
        wxASSERT(regions[x].start < regions[x].end);
        wxASSERT(regions[x].end <= text_len);
        if (x==0) {
            wxASSERT(regions[x].start == 0);
        }
        if (x>0) {
            wxASSERT(regions[x].start == regions[x-1].end);
            wxASSERT(regions[x].type != regions[x-1].type);
        }
        if (x == regions.size()-1) {
            wxASSERT(regions[x].end == text_len);
        }
    }
#endif
}

int TextLayout::LookupPos(int pos)
{
    wxASSERT(text_len>0 && regions.size()>0);
    wxASSERT(pos>=0 && pos<text_len);

    // binary search the position
    int search_from=0, search_to=regions.size(), search_at;
    while (search_from < search_to) {
        search_at = (search_from + search_to) / 2;
        if (pos < regions[search_at].start) {
            search_to = search_at - 1;
            continue;
        }
        if (pos >= regions[search_at].end) {
            search_from = search_at + 1;
            continue;
        }
        if (pos >= regions[search_at].start && pos < regions[search_at].end) {
            return search_at;
        }
    }
    wxASSERT(search_from == search_to);
    wxASSERT(search_from >= 0);
    wxASSERT(search_to < (int)regions.size());

    // check resulting position
    search_at = search_from;
    wxASSERT(pos >= regions[search_at].start && pos < regions[search_at].end);

    return search_at;
}

void TextLayout::SetRegion(TextRegion::Type type, int new_region_start, int new_region_end, const wxString& data)
{
    wxASSERT(text_len > 0);
    wxASSERT(new_region_start >= 0);
    wxASSERT(new_region_start < new_region_end);
    wxASSERT(new_region_end <= text_len);

    int start_index = LookupPos(new_region_start);
    int end_index = LookupPos(new_region_end-1);
    int new_region_index = start_index+1;

    // is the new region inside single existing region?
    if (start_index == end_index) {

        // are we exactly replacing it?
        if (new_region_start==regions[start_index].start && new_region_end==regions[end_index].end) {
            regions[start_index].type = type;
            regions[start_index].data = data;
            new_region_index = start_index;
            goto optimize_neighbors;
        }

        // duplicate region, so that rest of algorithm works nicely
        // (it assumes start_index < end_index)
        regions.insert(regions.begin()+start_index, TextRegion(regions[start_index]));
        end_index++;
    }

    // shorten ending region, unless it's entirely overwritten
    wxASSERT(new_region_end <= regions[end_index].end);
    if (new_region_end == regions[end_index].end) {
        regions.erase(regions.begin()+end_index);
        end_index--;
    } else {
        regions[end_index].start = new_region_end;
    }

    // erase all regions inbetween
    for (int x=end_index-1; x>start_index;x--)
        regions.erase(regions.begin()+x);

    // shorten starting region, unless it is entirely overwritten
    wxASSERT(new_region_start >= regions[start_index].start);
    if (new_region_start == regions[start_index].start) {
        regions.erase(regions.begin()+start_index);
        new_region_index--;
    } else {
        regions[start_index].end = new_region_start;
    }

    // create the new region
    regions.insert(regions.begin() + new_region_index, TextRegion(type, new_region_start, new_region_end, data));

optimize_neighbors:

    // if preceding region is of same type, expand new region over it
    if (new_region_index > 0 && regions[new_region_index-1].type == type && regions[new_region_index-1].data == data) {
        wxASSERT(regions[new_region_index-1].end == new_region_start);
        regions[new_region_index].start = regions[new_region_index-1].start;
        regions.erase(regions.begin() + new_region_index-1);
        new_region_index--;
    }

    // if following region is of same type, expand new region over it
    if (new_region_index < (int)regions.size()-1 && regions[new_region_index+1].type == type && regions[new_region_index+1].data == data) {
        wxASSERT(regions[new_region_index+1].start == new_region_end);
        regions[new_region_index].end = regions[new_region_index+1].end;
        regions.erase(regions.begin() + new_region_index+1);
    }

    // check result
    CheckIntegrity();
}

void TextLayout::AnalyzeMarkText(int from, int to)
{
    if (to == -1) to = text.Len();
    wxASSERT(from >= 0);
    wxASSERT(to <= (int)text.Len());
    wxASSERT(from < to);

    // scan paragraphs
    int paragraph_start, paragraph_end;     // inner parahraph body
    int whitespace_start, whitespace_end;   // paragraph including preceding and ending whitespace
    int pos=from;
    while (pos < to) {

        // ignore leading whitespace before paragraph
        whitespace_start = pos;
        while (pos < to  && IsWhitespaceChar(text[pos]))
            pos++;
        paragraph_start = pos;

        // end loop if there was only whitespace left
        if (pos == to)
            break;

        // scan paragraph
        while (pos<to && text[pos]!='\n')
            pos++;

        // skip whitespace at the end of paragraph
        whitespace_end = pos;
        paragraph_end = pos;
        while (paragraph_end > paragraph_start && IsWhitespaceChar(text[paragraph_end-1]))
            paragraph_end--;
        wxASSERT(paragraph_end > paragraph_start);

        // mark paragraph as note text
        SetRegion(TextRegion::Text, whitespace_start, whitespace_end);

        // analyze paragraph
        AnalyzeMarkText_Paragraph(paragraph_start, paragraph_end);
    }
}

void TextLayout::AnalyzeMarkText_Paragraph(int from, int to)
{
    if (to == -1) to = text.Len();
    wxASSERT(from >= 0 && from < (int)text.Len());
    wxASSERT(to > 0    && to <= (int)text.Len());
    wxASSERT(from < to);

    int in_parenth=0;
    int in_keyword=0, inline_keyword=0;
    int parenthesis_start=0, parenthesis_end=0, pos;
    int keyword_start=0, keyword_end=0, invalid_keyword=0, highlight_words=0;

    for (pos=from; pos<to; pos++) {


        if (text[pos] == '{' && !in_keyword) {
            in_keyword = 1;
            keyword_start = pos;
            invalid_keyword = 0;
            inline_keyword = 0;
            highlight_words = 0;
            if (pos+1 < to && text[pos+1] == '{') {
                inline_keyword = 1;
                pos++;
            }
            if (!inline_keyword)
                for (;pos+1<to && text[pos+1]=='<'; pos++) highlight_words++;
            continue;
        }

        if (text[pos] == '}') {
            if (!in_keyword) {
                SetRegion(TextRegion::Error, pos, pos+1);
            } else {
                in_keyword = 0;
                keyword_end = pos+1;

                // get inner keyword start (just the keyword, without " {<" and "}" parts)
                int inner_keyword_start = keyword_start, inner_keyword_end = pos;
                wxASSERT(text[inner_keyword_start]=='{');
                wxASSERT(text[inner_keyword_end]=='}');
                inner_keyword_start++;
                while (text[inner_keyword_start]=='<') inner_keyword_start++;

                // check for second } in inline keyword
                if (inline_keyword) {
                    if (pos+1 < to && text[pos+1] == '}') {
                        pos++;
                        keyword_end++;
                    } else
                        invalid_keyword=1;
                }

                // include preceding whitespace to keyword mark (so it disappears along with keyword mark)
                if (!inline_keyword)
                    while (keyword_start-1 >= from && IsWhitespaceChar(text[keyword_start-1])) keyword_start--;

                // get word(s) preceding keyword mark
                int word_start = keyword_start;
                int word_end = keyword_start;
                if (!inline_keyword && highlight_words>0) {

                    // and get the words preceding keyword mark
                    for (int word=0; word<highlight_words; word++) {

                        // skip preceding whitespace
                        while (word_start-1 >= from && IsWhitespaceChar(text[word_start-1])) word_start--;

                        // get preceding word
                        while (word_start-1 >= from && IsKeywordChar(text[word_start-1])) word_start--;

                        // not enough preceding words?
                        if (word_start == from && word < highlight_words-1) {
                            invalid_keyword=1;
                            break;
                        }
                    }

                    // no preceding word found?
                    if (word_start == word_end)
                        invalid_keyword=1;
                }

                // set regions according to keyword type
                if (invalid_keyword)
                    SetRegion(TextRegion::Error, keyword_start, keyword_end);
                else {
                    if (inline_keyword) {
                        wxASSERT(keyword_start+2 < keyword_end-2);
                        wxString keyword_text = text.Mid(keyword_start+2, keyword_end-keyword_start-4);
                        SetRegion(TextRegion::KeywordMark, keyword_start, keyword_start+2);
                        SetRegion(TextRegion::Keyword, keyword_start+2, keyword_end-2, keyword_text);
                        SetRegion(TextRegion::KeywordMark, keyword_end-2, keyword_end);
                    } else {
                        SetRegion(TextRegion::KeywordMark, keyword_start, keyword_end);
                        if (highlight_words > 0) {
                            wxASSERT(word_start >= 0 && word_start < word_end && word_end <= keyword_start);
                            wxString keyword_text = text.Mid(inner_keyword_start, inner_keyword_end-inner_keyword_start);
                            SetRegion(TextRegion::Keyword, word_start, word_end, keyword_text);
                        }
                    }
                }
            }
        }

        if (in_keyword && !IsKeywordChar(text[pos]) && !IsWhitespaceChar(text[pos]))
            invalid_keyword = 1;

        if (text[pos] == '(' && !in_keyword) {
            if (in_parenth)
                SetRegion(TextRegion::Error, parenthesis_start, parenthesis_start+1);
            parenthesis_start = pos;
            in_parenth=1;
            continue;
        }

        if (text[pos] == ')' && !in_keyword) {

            // if we occur mismatched ending parenthesis, mark it as error
            if (!in_parenth) {
                SetRegion(TextRegion::Error, pos, pos+1);
                continue;
            }

            in_parenth=0;

            parenthesis_end = pos+1;
            AnalyzeMarkText_Parenthesis(parenthesis_start, parenthesis_end);
        }
    }

    // if we are still inside parenthesis, mark it as error
    if (in_parenth)
        SetRegion(TextRegion::Error, parenthesis_start, parenthesis_start+1);
}

void TextLayout::AnalyzeMarkText_Parenthesis(int from, int to)
{
    if (IsSourceParenthesis(text, from, to))
        SetRegion(TextRegion::Source, from, to);
}

bool TextLayout::IsSourceParenthesis(const wxString& text, int from, int to)
{
    if (to == -1) to = text.Len();
    wxASSERT(from >= 0);
    wxASSERT(to <= (int)text.Len());
    wxASSERT(from+1 <= to-1);
    wxASSERT(text[from]=='(' && text[to-1]==')');

    // body
    int body_from = from+1;
    int body_to = to-1;

    // empty parenthesis
    if (body_from == body_to)
        return false;

    wxString par_text = text.Mid(body_from, body_to - body_from);

    // source parenthesis can't contain {} (keyword delimiters)
    if (par_text.Find('{') != wxNOT_FOUND || par_text.Find('}') != wxNOT_FOUND)
        return false;

    // is this source parenthesis?
    if (par_text.First(", s. ")!=-1 || par_text.First("; podla ")!=-1 || par_text.First("s. ")==0 ||
        par_text.First("http:")!=-1 || par_text.StartsWith("zdroj:") || par_text.First(", f. ")!=-1 ||
        par_text.StartsWith("SNA, ") || par_text.StartsWith("NACR, ") || par_text.StartsWith("A UPN, ") ||
        par_text.StartsWith("VHA, ") || par_text.StartsWith("SOBA, ") || par_text.StartsWith("SABA, ")
        )
    {
        return true;
    }

    return false;
}

void TextLayout::AppendRegion(const wxString& new_text, TextRegion::Type region_type, const wxString& region_data)
{
    int orig_len = text_len, new_len = new_text.Len();
    if (new_len == 0)
        return;

    text.Append(new_text);
    text_len += new_len;

    // first create unknown region, then use SetRegion() for its same-region-merging abilities
    regions.push_back(TextRegion(TextRegion::Unknown, orig_len, orig_len+new_len));
    SetRegion(region_type, orig_len, orig_len+new_len, region_data);
}

/// Appends and analyzes mark text
void TextLayout::AppendMarkText(const wxString& mark_text)
{
    int orig_len = text_len, new_len = mark_text.Len();
    if (new_len == 0)
        return;

    text.Append(mark_text);
    text_len += new_len;

    // first create unknown region, then use SetRegion() for its same-region-merging abilities
    regions.push_back(TextRegion(TextRegion::Unknown, orig_len, orig_len+new_len));
    AnalyzeMarkText(orig_len, orig_len + new_len);
}
